8장. 재생 제어권의 이동 — Flash에서 MSE까지
8.1 시점 전환: 누가 재생을 통제하는가
지금까지 우리는 서버와 전송 구조를 중심으로 살펴봤다.
이제 시점을 바꿔야 한다.
브라우저는 이 데이터를 어떻게 재생하는가
여기서 중요한 관점은 하나다.
스트리밍 기술은 단순히 포맷이 바뀐 것이 아니라, “재생을 누가 통제하느냐“가 계속 바뀌어 온 과정이다
이 흐름은 Flash → HTML5 video → MSE로 이어진다.
8.2 Flash 시대: 모든 제어권은 어도비에
초기 웹 스트리밍은 Flash가 중심이었다.
브라우저는 Flash Player를 실행해주는 역할만 하고 실제 재생은 Flash 내부에서 이루어졌다.
브라우저 → Flash 실행
Flash → 서버와 통신 (RTMP)
Flash → 직접 재생
이 구조에서 제어권은 명확했다.
모든 제어권은 어도비(Flash)에 있었다.
Flash의 장점
- 실시간 스트리밍 가능 (RTMP)
- 플레이어를 자유롭게 구현
- 강력한 영상 처리 기능
Flash의 단점
- 플러그인 설치 필요
- 보안 취약점 다수
- CPU / 배터리 소모 큼
- 모바일, 특히 iOS에서 지원 안 됨
특히 마지막이 결정타였다.
8.3 전환점: 애플의 선택과 HLS
애플은 iPhone에서 Flash를 지원하지 않기로 결정했다.
이건 단순한 기술 문제가 아니라 플랫폼 주도권, 보안, 효율성을 고려한 전략적 선택이었다.
Flash 대신 애플이 밀어붙인 것이 HLS다.
이 선택으로 스트리밍 방식이 바뀐다.
- RTMP → HTTP 기반
- 플러그인 → 브라우저 기본 기능
그리고 재생 구조도 바뀐다.
Flash 내부가 아니라, 브라우저가 직접 재생을 담당하게 된다.
8.4 HTML5 video 시대
Flash 이후 등장한 것이 HTML5 video 태그다.
<video src="stream.m3u8" controls></video>
매우 단순하다.
개발자는 영상의 위치(URL)만 넘겨주면 된다.
그러면 브라우저가 내부적으로:
- m3u8 다운로드
- Segment 목록 확인 및 순차 다운로드
- 내부 버퍼 관리 및 디코딩
- 화면 렌더링
을 모두 처리한다.
브라우저가 다운로드부터 재생까지 모든 것을 처리한다.
이 구조에서 제어권은 이렇게 바뀐다.
어도비 → 브라우저
새로 생긴 한계
이 방식은 표준 기반이고 모바일에서도 잘 동작했다.
하지만 새로운 한계가 생긴다.
- 언제 데이터를 가져올지 제어 불가
- buffer 크기 제어 불가
- 지연 시간 제어 불가
- Low Latency 구현 어려움
즉 개발자는 단순히
“이 영상 틀어줘”
라고 요청만 할 수 있고 재생 알고리즘에는 개입할 수 없다.
8.5 Low Latency에서 드러난 한계
LL-HLS 구조에서는 상황이 달라진다.
데이터가 더 이상 완성된 파일로 오지 않는다. 작은 chunk 단위로 계속 도착한다.
이걸 제대로 재생하려면:
- chunk가 오면 바로 재생에 반영
- buffer를 최소화
- 지연을 계속 조절
하지만 HTML5 video 구조에서는 불가능하다.
왜냐하면
브라우저가 “파일 단위“로 처리하기 때문이다.
브라우저는 chunk 흐름을 인지하지 않는다. 이 모델로는 LL-HLS의 잠재력을 못 살린다.
새로운 방식이 필요해진다.
8.6 MSE: 데이터를 직접 넣는 방식
MSE (Media Source Extensions)는 이 문제를 해결하기 위해 등장했다.
기존 방식과 가장 큰 차이는 이것이다.
URL을 넘기는 방식에서 바이너리 데이터를 직접 넘기는 방식으로 바뀐다
기존 HTML5 video 방식
개발자 → URL 전달
브라우저 → 알아서 다운로드 + 버퍼링 + 재생
MSE 방식
JavaScript → 직접 데이터 다운로드 (fetch)
JavaScript → buffer에 데이터 직접 추가
브라우저 → 재생만 수행
즉 구조가 이렇게 바뀐다.
개발자가 데이터를 가져오고 브라우저는 그걸 재생만 한다.
비유
기존 방식:
“이 주소에 있는 영상 틀어줘”
MSE 방식:
“영상 데이터를 내가 가져왔으니까 이걸 그대로 틀어줘”
이 변화의 의미는 크다.
재생 제어권이 개발자에게 넘어간다.
8.7 MSE API 실제 흐름
여기서는 MSE를 실제로 어떻게 쓰는지 본다.
8.7.1 MediaSource와 SourceBuffer
MSE는 두 가지 핵심 객체로 구성된다.
const video = document.querySelector('video');
const mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);
MediaSource = 가상의 영상 소스
SourceBuffer = 데이터를 담는 버퍼
MediaSource는 여러 SourceBuffer를 가질 수 있다.
(보통 비디오용 1개, 오디오용 1개)
8.7.2 addSourceBuffer / appendBuffer / updateend
기본 사용 흐름:
mediaSource.addEventListener('sourceopen', () => {
// 1) SourceBuffer 생성
const sourceBuffer = mediaSource.addSourceBuffer(
'video/mp4; codecs="avc1.640028, mp4a.40.2"'
);
// 2) init.mp4 다운로드 후 buffer에 추가
fetch('init.mp4')
.then(r => r.arrayBuffer())
.then(data => sourceBuffer.appendBuffer(data));
// 3) 각 segment를 차례로 추가
sourceBuffer.addEventListener('updateend', () => {
if (queue.length > 0) {
const next = queue.shift();
fetch(next)
.then(r => r.arrayBuffer())
.then(data => sourceBuffer.appendBuffer(data));
}
});
});
핵심 메서드:
| 메서드 | 역할 |
|---|---|
addSourceBuffer(mimeType) | 새 SourceBuffer 생성 |
appendBuffer(data) | 바이너리 데이터 추가 |
remove(start, end) | 시간 범위 삭제 (메모리 관리) |
endOfStream() | 더 이상 데이터 없음 알림 |
핵심 이벤트:
| 이벤트 | 발생 시점 |
|---|---|
sourceopen | MediaSource 사용 준비 완료 |
updateend | appendBuffer/remove 작업 완료 |
error | 디코딩 실패 등 |
8.7.3 init segment를 먼저 넣어야 하는 이유
7장에서 본 fMP4 구조를 떠올려보자.
init.mp4 ← 코덱 정보, 트랙 메타데이터
moof + mdat ← 본체
브라우저(디코더)는 init이 없으면 본체를 해석할 수 없다.
따라서 MSE도 동일한 순서를 요구한다.
1. appendBuffer(init.mp4) ← 반드시 먼저
2. appendBuffer(segment1.m4s)
3. appendBuffer(segment2.m4s)
순서를 바꾸면
error 이벤트가 발생한다.
8.7.4 SourceBuffer mode: segments vs sequence
SourceBuffer에는 mode라는 속성이 있다.
sourceBuffer.mode = 'segments'; // 기본값
// 또는
sourceBuffer.mode = 'sequence';
| mode | 동작 |
|---|---|
segments | segment에 담긴 타임스탬프를 그대로 사용 |
sequence | 들어온 순서대로 이어 붙임 (타임스탬프 무시) |
라이브 스트리밍에서는 보통 segments.
임의의 클립을 이어 붙일 때는 sequence.
8.7.5 buffer 제거와 메모리 관리
오래 재생할수록 buffer가 쌓인다.
브라우저는 메모리 한계에 부딪히면 다음 에러를 던진다.
QuotaExceededError
이를 방지하려면 오래된 부분을 직접 삭제한다.
// 현재 시점에서 60초 이전 데이터를 삭제
const currentTime = video.currentTime;
sourceBuffer.remove(0, currentTime - 60);
라이브에서는 보통 현재 시점 ± 일정 범위만 유지한다.
MSE 코드 전체 흐름 (요약)
1. MediaSource 생성
2. video.src에 URL.createObjectURL로 연결
3. sourceopen 이벤트 대기
4. addSourceBuffer로 비디오/오디오 SourceBuffer 생성
5. init segment를 appendBuffer
6. 각 segment(또는 part)를 순서대로 appendBuffer
7. 필요하면 remove로 오래된 buffer 정리
8. 끝나면 endOfStream
8.8 hls.js는 무엇을 해주는가
위 코드를 직접 짜는 건 무겁다.
- m3u8 파싱
- segment URL 계산
- TS → fMP4 변환 (브라우저는 TS 직접 재생 불가)
- ABR 알고리즘
- 에러 복구
이걸 모두 해주는 라이브러리가 hls.js다.
hls.js 내부 흐름 (요약)
1. m3u8 파일 다운로드 + 파싱
2. Master playlist → variant 선택
3. Media playlist 주기적 재요청
4. 각 segment fetch
5. TS인 경우 → 내부에서 fMP4로 변환 (demux + remux)
6. fMP4 chunk를 MSE SourceBuffer에 appendBuffer
7. ABR 알고리즘으로 다음 화질 결정
8. 에러 발생 시 재시도 / fallback
복잡한 부분은 모두 라이브러리가 처리한다.
개발자는 보통 이렇게만 쓴다.
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource('master.m3u8');
hls.attachMedia(video);
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari: 네이티브 HLS 재생
video.src = 'master.m3u8';
}
비슷한 라이브러리들
| 라이브러리 | 특징 |
|---|---|
| hls.js | HLS 전용. 가장 많이 쓰임 |
| Shaka Player | HLS + DASH 모두 지원. Google이 만듦 |
| Video.js | UI/UX 중심. 다양한 플러그인 |
| dash.js | DASH 전용 레퍼런스 구현체 |
8.9 Safari는 왜 다른 구조를 가지는가
여기서 Safari가 자연스럽게 등장한다.
지금까지 흐름은 “개발자가 점점 더 많은 제어권을 가진다“는 방향이었다.
그런데 Safari는 이 흐름을 매우 늦게 수용했다.
이유: 애플의 철학
애플은 HLS의 종주국이다.
iOS / macOS 시스템 전체(AVFoundation)가
HLS 재생에 최적화되어 있다.
애플은 배터리 효율과 시스템 안정성을 위해 개발자가 하드웨어를 직접 제어하는 MSE 방식을 오랫동안 제한했다.
video 태그 → m3u8 URL
Safari 내부 플레이어가 처리
이 구조에서는 재생 제어권이 여전히 브라우저 내부에 있다.
다른 브라우저는 왜 MSE로 갔나
- 네이티브 스트리밍 기능이 부족했다
- HLS 네이티브 재생 미지원
- 자유로운 플레이어 구현 필요
이런 사정 때문에 Chrome / Firefox / Edge는 MSE 기반으로 발전했다.
Safari는 이미 완성된 HLS 플레이어를 가지고 있었기에 그 구조를 유지했다.
8.10 실무에서 발생하는 차이
이 구조 차이는 실제 서비스에서 차이를 만든다.
Chrome / Firefox / Edge
- MSE 기반
- 개발자가 buffer / 지연 / 화질을 직접 제어
개발자가 버퍼를 2초로 강제하고 지연을 조절할 수 있다
Safari (iOS)
- Native HLS 기반
- 내부 로직에 따라 재생
개발자가 아무리 요청해도 iOS 시스템 플레이어가 “안정성을 위해 버퍼를 5초 이상 쌓겠다“라고 판단하면 개발자가 개입할 수 없다.
이런 차이가 만들어내는 결과:
- 같은 LL-HLS 스트림인데 Chrome은 1.5초 지연, Safari는 4초 지연이 나온다
- 개발자가 Safari의 지연을 줄일 직접적인 방법이 없다
따라서:
Safari는 “성능이 부족한 것“이 아니라, “시스템이 제어권을 꽉 쥐고 놓아주지 않는 것“이다.
8.11 Managed Media Source (iOS 17+)
최근 변화가 있다.
iOS 17 / macOS Sonoma부터 Safari는 Managed Media Source (MMS)를 도입했다.
이는 MSE의 변형이다.
개발자가 데이터를 직접 공급하지만, 메모리/배터리 관리는 시스템이 한다.
MSE vs MMS
| 항목 | MSE | MMS |
|---|---|---|
| 데이터 공급 | 개발자 | 개발자 |
| 메모리 관리 | 개발자 | 시스템 |
| 배터리 관리 | 개발자 | 시스템 |
| 가능한 곳 | Chrome 등 | Safari (iOS 17+) |
이 변화로 Safari에서도 LL-HLS를 더 적극적으로 다룰 수 있게 되었다.
다만:
- iOS 17 미만 기기는 여전히 native HLS만
- MMS는 MSE의 1:1 대체가 아니라 제약형
완전한 통일까지는 시간이 걸린다.
8.12 핵심 정리
이 장의 핵심을 정리하면 이렇다.
재생 제어권의 이동
Flash → 어도비가 통제
HTML5 video → 브라우저가 통제
MSE → 개발자가 통제
MMS → 개발자 + 시스템 협업
그리고 가장 중요한 한 문장:
Low Latency는 “데이터를 어떻게 보내느냐“와 “누가 재생을 제어하느냐“가 함께 만들어낸 결과다.
서버 측 구조(LL-HLS, CMAF)만 갖춰져도 재생 측 제어권이 없으면 초저지연을 만들 수 없다.
이 두 축이 함께 진화해 왔다는 사실이 이 책의 가장 큰 그림이다.
8장 한 줄 정리
스트리밍의 진화는 누구의 손에 재생이 있는가를 끊임없이 옮겨 온 역사다.